import librosa
import numpy as np
from pydub import AudioSegment
from mutagen.mp4 import MP4
import soundfile as sf
import matplotlib.pyplot as plt
from scipy import signal
from scipy.fft import fft, fftfreq
import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import librosa.display

warnings.filterwarnings('ignore')
class M4AAnalyzer:
    def __init__(self, file_path):
        self.file_path = file_path
        self.audio_data = None
        self.sample_rate = None
        
    def load_audio(self):
        """Load audio file using librosa"""
        print("Loading audio file...")
        self.audio_data, self.sample_rate = librosa.load(self.file_path, sr=None, mono=False)
        print(f"✓ Audio loaded successfully\n")
        
    def get_file_metadata(self):
        """Extract metadata using mutagen"""
        print("=" * 60)
        print("FILE METADATA")
        print("=" * 60)
        
        try:
            audio = MP4(self.file_path)
            
            # Basic file info
            print(f"File: {self.file_path}")
            print(f"Format: M4A (MPEG-4 Audio)")
            
            # Metadata tags
            if audio.tags:
                print("\nMetadata Tags:")
                for key, value in audio.tags.items():
                    print(f"  {key}: {value}")
            else:
                print("\nNo metadata tags found")
                
            # Technical info
            print(f"\nBitrate: {audio.info.bitrate} bps ({audio.info.bitrate/1000:.2f} kbps)")
            print(f"Sample Rate: {audio.info.sample_rate} Hz")
            print(f"Channels: {audio.info.channels}")
            print(f"Duration: {audio.info.length:.2f} seconds")
            print(f"Bits Per Sample: {audio.info.bits_per_sample}")
            
        except Exception as e:
            print(f"Error reading metadata: {e}")
    
    def get_audio_properties(self):
        """Analyze basic audio properties"""
        print("\n" + "=" * 60)
        print("AUDIO PROPERTIES")
        print("=" * 60)
        
        # Handle mono/stereo
        if len(self.audio_data.shape) == 1:
            audio_mono = self.audio_data
            is_stereo = False
        else:
            audio_mono = np.mean(self.audio_data, axis=0)
            is_stereo = True
            
        duration = len(audio_mono) / self.sample_rate
        
        print(f"Sample Rate: {self.sample_rate} Hz")
        print(f"Channels: {'Stereo (2)' if is_stereo else 'Mono (1)'}")
        print(f"Duration: {duration:.2f} seconds ({duration/60:.2f} minutes)")
        print(f"Total Samples: {len(audio_mono):,}")
        print(f"Bit Depth: 32-bit float (after loading)")
        
        # Amplitude statistics
        print(f"\nAmplitude Statistics:")
        print(f"  Maximum: {np.max(np.abs(audio_mono)):.6f}")
        print(f"  Mean: {np.mean(np.abs(audio_mono)):.6f}")
        print(f"  RMS: {np.sqrt(np.mean(audio_mono**2)):.6f}")
        print(f"  Peak-to-Peak: {np.ptp(audio_mono):.6f}")
        
    def analyze_frequency_content(self):
        """Analyze frequency spectrum"""
        print("\n" + "=" * 60)
        print("FREQUENCY ANALYSIS")
        print("=" * 60)
        
        if len(self.audio_data.shape) == 1:
            audio_mono = self.audio_data
        else:
            audio_mono = np.mean(self.audio_data, axis=0)
        
        # Spectral centroid
        spectral_centroids = librosa.feature.spectral_centroid(y=audio_mono, sr=self.sample_rate)[0]
        print(f"Spectral Centroid (mean): {np.mean(spectral_centroids):.2f} Hz")
        
        # Spectral rolloff
        spectral_rolloff = librosa.feature.spectral_rolloff(y=audio_mono, sr=self.sample_rate)[0]
        print(f"Spectral Rolloff (mean): {np.mean(spectral_rolloff):.2f} Hz")
        
        # Spectral bandwidth
        spectral_bandwidth = librosa.feature.spectral_bandwidth(y=audio_mono, sr=self.sample_rate)[0]
        print(f"Spectral Bandwidth (mean): {np.mean(spectral_bandwidth):.2f} Hz")
        
        # Zero crossing rate
        zcr = librosa.feature.zero_crossing_rate(audio_mono)[0]
        print(f"Zero Crossing Rate (mean): {np.mean(zcr):.6f}")
        
        # Dominant frequency
        fft_vals = np.abs(fft(audio_mono))
        fft_freq = fftfreq(len(audio_mono), 1/self.sample_rate)
        dominant_freq = fft_freq[np.argmax(fft_vals[:len(fft_vals)//2])]
        print(f"Dominant Frequency: {abs(dominant_freq):.2f} Hz")
        
    def analyze_rhythm_tempo(self):
        """Analyze rhythm and tempo"""
        print("\n" + "=" * 60)
        print("RHYTHM & TEMPO ANALYSIS")
        print("=" * 60)
        
        if len(self.audio_data.shape) == 1:
            audio_mono = self.audio_data
        else:
            audio_mono = np.mean(self.audio_data, axis=0)
        
        # Tempo detection
        tempo, beats = librosa.beat.beat_track(y=audio_mono, sr=self.sample_rate)
        try:
            tempo_val = float(np.atleast_1d(tempo)[0])
            print(f"Estimated Tempo: {tempo:.2f} BPM")
        except Exception:
            print(f"Estimated Tempo: {tempo}")
            
        print(f"Number of Beats Detected: {len(beats)}")
        
        # Onset detection
        onset_env = librosa.onset.onset_strength(y=audio_mono, sr=self.sample_rate)
        onsets = librosa.onset.onset_detect(y=audio_mono, sr=self.sample_rate)
        print(f"Number of Onsets Detected: {len(onsets)}")
        
    def analyze_dynamic_range(self):
        """Analyze dynamic range and loudness"""
        print("\n" + "=" * 60)
        print("DYNAMIC RANGE & LOUDNESS")
        print("=" * 60)
        
        if len(self.audio_data.shape) == 1:
            audio_mono = self.audio_data
        else:
            audio_mono = np.mean(self.audio_data, axis=0)
        
        # RMS energy
        rms = librosa.feature.rms(y=audio_mono)[0]
        print(f"RMS Energy (mean): {np.mean(rms):.6f}")
        print(f"RMS Energy (max): {np.max(rms):.6f}")
        print(f"RMS Energy (min): {np.min(rms):.6f}")
        
        # Dynamic range in dB
        rms_db = librosa.amplitude_to_db(rms)
        dynamic_range = np.max(rms_db) - np.min(rms_db)
        print(f"Dynamic Range: {dynamic_range:.2f} dB")
        
        # Peak amplitude in dB
        peak_db = librosa.amplitude_to_db(np.array([np.max(np.abs(audio_mono))]))
        print(f"Peak Level: {peak_db[0]:.2f} dB")
        
        # Crest factor
        crest_factor = np.max(np.abs(audio_mono)) / np.sqrt(np.mean(audio_mono**2))
        print(f"Crest Factor: {crest_factor:.2f}")
        
    def analyze_spectral_features(self):
        """Analyze advanced spectral features"""
        print("\n" + "=" * 60)
        print("SPECTRAL FEATURES")
        print("=" * 60)
        
        if len(self.audio_data.shape) == 1:
            audio_mono = self.audio_data
        else:
            audio_mono = np.mean(self.audio_data, axis=0)
        
        # MFCCs
        mfccs = librosa.feature.mfcc(y=audio_mono, sr=self.sample_rate, n_mfcc=13)
        print(f"MFCCs (13 coefficients):")
        for i, mfcc in enumerate(mfccs):
            print(f"  MFCC {i+1} (mean): {np.mean(mfcc):.4f}")
        
        # Spectral contrast
        contrast = librosa.feature.spectral_contrast(y=audio_mono, sr=self.sample_rate)
        print(f"\nSpectral Contrast (mean): {np.mean(contrast):.4f}")
        
        # Chroma features
        chroma = librosa.feature.chroma_stft(y=audio_mono, sr=self.sample_rate)
        print(f"Chroma Features (mean): {np.mean(chroma):.4f}")
        
    def detect_silence(self):
        """Detect silent portions"""
        print("\n" + "=" * 60)
        print("SILENCE DETECTION")
        print("=" * 60)
        
        if len(self.audio_data.shape) == 1:
            audio_mono = self.audio_data
        else:
            audio_mono = np.mean(self.audio_data, axis=0)
        
        # Define silence threshold (in dB)
        silence_threshold = -40
        
        # Convert to dB
        audio_db = librosa.amplitude_to_db(np.abs(audio_mono), ref=np.max)
        
        # Find silent frames
        silent_frames = audio_db < silence_threshold
        silence_percentage = (np.sum(silent_frames) / len(silent_frames)) * 100
        
        print(f"Silence Threshold: {silence_threshold} dB")
        print(f"Silence Percentage: {silence_percentage:.2f}%")
        
    def analyze_all(self):
        """Run all analyses"""
        self.load_audio()
        self.get_file_metadata()
        self.get_audio_properties()
        self.analyze_frequency_content()
        self.analyze_dynamic_range()
        self.analyze_spectral_features()
        self.detect_silence()
        self.analyze_rhythm_tempo()
        
        print("\n" + "=" * 60)
        print("ANALYSIS COMPLETE")
        print("=" * 60)

# Usage example
if __name__ == "__main__":
    # Replace with your M4A file path
    file_path = r"E:/AUDIOnoise.m4a"
    
    analyzer = M4AAnalyzer(file_path)
    analyzer.analyze_all()
    
    # For individual analyses:
    # analyzer.load_audio()
    # analyzer.get_file_metadata()
    # analyzer.analyze_frequency_content()
    # etc.

# --- 3. FAST METADATA (Mutagen) ---
try:
    audio_info = MP4(file_path).info
    print(f"Metadata: {audio_info.length:.2f}s, {audio_info.bitrate//1000}kbps")
except Exception as e:
    print(f"Metadata Warning: Could not read header ({e})")

# --- 4. SIGNAL LOADING (Defining 'y' and 'sr') ---
print("Loading audio signal...")
try:
    # We use sr=22050 to speed up processing while maintaining quality
    y, sr = librosa.load(file_path, sr=22050) 
    print("Signal loaded successfully.")
except Exception as e:
    print(f"FATAL ERROR: librosa could not load the file. \nReason: {e}")
    print("\nPRO TIP: Ensure FFmpeg is installed and you restarted your IDE.")
    sys.exit()

# --- 5. ANALYSIS (Now 'y' is guaranteed to exist) ---
# Time Spectrum
D = np.abs(librosa.stft(y))
S_db = librosa.amplitude_to_db(D, ref=np.max)

# Intensity & Positive Normalization
rms = librosa.feature.rms(y=y)[0]
intensity_db_neg = librosa.amplitude_to_db(rms, ref=np.max, top_db=80)
intensity_positive = intensity_db_neg - np.min(intensity_db_neg)

# Power Spectrum
mean_power_db = librosa.amplitude_to_db(np.mean(D, axis=1), ref=np.max)
freqs = librosa.fft_frequencies(sr=sr)


# --- 6. EXPORT & PLOTS ---
times = librosa.times_like(rms, sr=sr)
pd.DataFrame({'Time': times, 'Intensity_Pos': intensity_positive}).to_csv("analysis_results.csv", index=False)

plt.figure(figsize=(12, 8))
plt.subplot(2, 1, 1)
librosa.display.specshow(S_db, x_axis='time', y_axis='log', sr=sr)
plt.title("Time Spectrum (Spectrogram)")
